import { Map } from '../Map';
import { Handler } from '../../core/Handler';
import { Draggable } from '../../dom/Draggable';
import * as Util from '../../core/Util';
import * as DomUtil from '../../dom/DomUtil';
import { toLatLngBounds as latLngBounds } from '../../geo/LatLngBounds';
import { toBounds } from '../../geometry/Bounds';

/*
 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
 */

// @namespace Map
// @section Interaction Options
Map.mergeOptions({
  // @option dragging: Boolean = true
  // Whether the map is draggable with mouse/touch or not.
  dragging: true,

  // @section Panning Inertia Options
  // @option inertia: Boolean = *
  // If enabled, panning of the map will have an inertia effect where
  // the map builds momentum while dragging and continues moving in
  // the same direction for some time. Feels especially nice on touch
  // devices. Enabled by default.
  inertia: true,

  // @option inertiaDeceleration: Number = 3000
  // The rate with which the inertial movement slows down, in pixels/second².
  inertiaDeceleration: 3400, // px/s^2

  // @option inertiaMaxSpeed: Number = Infinity
  // Max speed of the inertial movement, in pixels/second.
  inertiaMaxSpeed: Infinity, // px/s

  // @option easeLinearity: Number = 0.2
  easeLinearity: 0.2,

  // TODO refactor, move to CRS
  // @option worldCopyJump: Boolean = false
  // With this option enabled, the map tracks when you pan to another "copy"
  // of the world and seamlessly jumps to the original one so that all overlays
  // like markers and vector layers are still visible.
  worldCopyJump: false,

  // @option maxBoundsViscosity: Number = 0.0
  // If `maxBounds` is set, this option will control how solid the bounds
  // are when dragging the map around. The default value of `0.0` allows the
  // user to drag outside the bounds at normal speed, higher values will
  // slow down map dragging outside bounds, and `1.0` makes the bounds fully
  // solid, preventing the user from dragging outside the bounds.
  maxBoundsViscosity: 0.0,
});

export var Drag = Handler.extend({
  addHooks: function () {
    if (!this._draggable) {
      var map = this._map;

      this._draggable = new Draggable(map._mapPane, map._container);

      this._draggable.on(
        {
          dragstart: this._onDragStart,
          drag: this._onDrag,
          dragend: this._onDragEnd,
        },
        this,
      );

      this._draggable.on('predrag', this._onPreDragLimit, this);
      if (map.options.worldCopyJump) {
        this._draggable.on('predrag', this._onPreDragWrap, this);
        map.on('zoomend', this._onZoomEnd, this);

        map.whenReady(this._onZoomEnd, this);
      }
    }
    DomUtil.addClass(this._map._container, 'leaflet-grab leaflet-touch-drag');
    this._draggable.enable();
    this._positions = [];
    this._times = [];
  },

  removeHooks: function () {
    DomUtil.removeClass(this._map._container, 'leaflet-grab');
    DomUtil.removeClass(this._map._container, 'leaflet-touch-drag');
    this._draggable.disable();
  },

  moved: function () {
    return this._draggable && this._draggable._moved;
  },

  moving: function () {
    return this._draggable && this._draggable._moving;
  },

  _onDragStart: function () {
    var map = this._map;

    map._stop();
    if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
      var bounds = latLngBounds(this._map.options.maxBounds);

      this._offsetLimit = toBounds(
        this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
        this._map
          .latLngToContainerPoint(bounds.getSouthEast())
          .multiplyBy(-1)
          .add(this._map.getSize()),
      );

      this._viscosity = Math.min(
        1.0,
        Math.max(0.0, this._map.options.maxBoundsViscosity),
      );
    } else {
      this._offsetLimit = null;
    }

    map.fire('movestart').fire('dragstart');

    if (map.options.inertia) {
      this._positions = [];
      this._times = [];
    }
  },

  _onDrag: function (e) {
    if (this._map.options.inertia) {
      var time = (this._lastTime = +new Date()),
        pos = (this._lastPos =
          this._draggable._absPos || this._draggable._newPos);

      this._positions.push(pos);
      this._times.push(time);

      this._prunePositions(time);
    }

    this._map.fire('move', e).fire('drag', e);
  },

  _prunePositions: function (time) {
    while (this._positions.length > 1 && time - this._times[0] > 50) {
      this._positions.shift();
      this._times.shift();
    }
  },

  _onZoomEnd: function () {
    var pxCenter = this._map.getSize().divideBy(2),
      pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);

    this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
    this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
  },

  _viscousLimit: function (value, threshold) {
    return value - (value - threshold) * this._viscosity;
  },

  _onPreDragLimit: function () {
    if (!this._viscosity || !this._offsetLimit) {
      return;
    }

    var offset = this._draggable._newPos.subtract(this._draggable._startPos);

    var limit = this._offsetLimit;
    if (offset.x < limit.min.x) {
      offset.x = this._viscousLimit(offset.x, limit.min.x);
    }
    if (offset.y < limit.min.y) {
      offset.y = this._viscousLimit(offset.y, limit.min.y);
    }
    if (offset.x > limit.max.x) {
      offset.x = this._viscousLimit(offset.x, limit.max.x);
    }
    if (offset.y > limit.max.y) {
      offset.y = this._viscousLimit(offset.y, limit.max.y);
    }

    this._draggable._newPos = this._draggable._startPos.add(offset);
  },

  _onPreDragWrap: function () {
    // TODO refactor to be able to adjust map pane position after zoom
    var worldWidth = this._worldWidth,
      halfWidth = Math.round(worldWidth / 2),
      dx = this._initialWorldOffset,
      x = this._draggable._newPos.x,
      newX1 = ((x - halfWidth + dx) % worldWidth) + halfWidth - dx,
      newX2 = ((x + halfWidth + dx) % worldWidth) - halfWidth - dx,
      newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;

    this._draggable._absPos = this._draggable._newPos.clone();
    this._draggable._newPos.x = newX;
  },

  _onDragEnd: function (e) {
    var map = this._map,
      options = map.options,
      noInertia = !options.inertia || e.noInertia || this._times.length < 2;

    map.fire('dragend', e);

    if (noInertia) {
      map.fire('moveend');
    } else {
      this._prunePositions(+new Date());

      var direction = this._lastPos.subtract(this._positions[0]),
        duration = (this._lastTime - this._times[0]) / 1000,
        ease = options.easeLinearity,
        speedVector = direction.multiplyBy(ease / duration),
        speed = speedVector.distanceTo([0, 0]),
        limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
        limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
        decelerationDuration =
          limitedSpeed / (options.inertiaDeceleration * ease),
        offset = limitedSpeedVector
          .multiplyBy(-decelerationDuration / 2)
          .round();

      if (!offset.x && !offset.y) {
        map.fire('moveend');
      } else {
        offset = map._limitOffset(offset, map.options.maxBounds);

        Util.requestAnimFrame(function () {
          map.panBy(offset, {
            duration: decelerationDuration,
            easeLinearity: ease,
            noMoveStart: true,
            animate: true,
          });
        });
      }
    }
  },
});

// @section Handlers
// @property dragging: Handler
// Map dragging handler (by both mouse and touch).
Map.addInitHook('addHandler', 'dragging', Drag);
